/*
 * Copyright (c) 2008-2009, Computational Crawling LP
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
 *
 *    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 *    * Neither the name of Computational Crawling LP, 80legs, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.eightylegs.customer.processtest;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;
import java.util.Properties;

import com.eightylegs.customer.I80App;
import com.eightylegs.customer.Custom80AppFactory;

/**
 * Class Test80legsUserCode
 * 
 * This class handles the loading and testing of the customer's jar.  Typical usage would be:
 * 		Test80legsUserCode test = new Test80legsUserCode(jarFileName, dataFileName);
 *      test.testPage(docInfoFileName, docContentsFileName);
 */
public class Test80legsUserCode {
	private I80App customerAnalysis;

	/**
	 * Constructs a Test80legsUserCode object  
	 *    
	 * @param jarFileName - this file contains the user's WebAnalysis code jar to be tested
	 * @param dataFileName - this file contains the user's data to be passed into WebAnalysis.deserializeData()
	 */
	public Test80legsUserCode( String jarFileName, String dataFileName ) throws Exception {
		// load in the jar
		customerAnalysis = loadJar ( jarFileName, dataFileName );
	}

	/**
	 * tests a single document  
	 *    
	 * @param docInfoFileName - this file contains the information about the page to be tested (url, headers, and statusCode)
	 * @param docContentsFileName - this file contains the page to be tested
	 */
	public void testPage ( String docInfoFileName, String docContentsFileName ) throws Exception {
		System.out.println ( now() + " Testing page: " + docInfoFileName );
		
		// read the document info out of the file
		SimulatedPageInfo pageInfo = readPageInfo ( docInfoFileName );
	    
		// read all the binary contents of dataFileName
		byte[] docContents = readPageContents( docContentsFileName );

		// test the function
		testUserFunction ( pageInfo, docContents );
	}
	
	/**
	 * load's fileName and returns a WebAnalysis object created from it  
	 *    
	 * @param fileName - this file contains the user's WebAnalysis code jar to be tested
	 * @return a new WebAnalysis object created from fileName
	 */
	@SuppressWarnings("unchecked")
	private static I80App loadJar(String fileName, String dataFileName) throws Exception {
		try {
			URL[] urls = new URL[1];
			urls[0] = new URL("file:///" + fileName);		
			
		    Class[] parameters = new Class[]{URL.class};
		
		    URLClassLoader sysloader = (URLClassLoader)ClassLoader.getSystemClassLoader();
		    Class<URLClassLoader> sysclass = URLClassLoader.class;
	
	        Method method = sysclass.getDeclaredMethod("addURL", parameters);
	        method.setAccessible(true);
	        method.invoke(sysloader, (Object[])urls);
		}
		catch ( NoClassDefFoundError e ) {
			System.out.println( now() + " NoClassDefFoundError - Could not find jar file: " + fileName + "\n" );  
			throw ( e );
		}
		catch ( Exception e ) {
			System.out.println( now() + " Could not find jar file: " + fileName + "\n" );  
			throw ( e );
		}

        byte[] userData = null;
		try {
			// read all the binary contents of dataFileName
			if ( dataFileName != null ) {
				int fileSize = (int)new File(dataFileName).length();
		    	userData = new byte[fileSize];
				BufferedInputStream r = new BufferedInputStream(new FileInputStream(dataFileName));
				r.read(userData);
				r.close();
			}
		}
		catch ( Exception e ) {
			System.out.println( now() + " error reading user data file " + dataFileName + "\n" );  
			throw ( e );
		}
	     
		try {
			System.out.println ( now() + " Starting constructor and initialize()" );
			I80App webAnalysis = Custom80AppFactory.get80App();
			webAnalysis.initialize( new Properties(), userData );
			System.out.println ( now() + " Finished constructor and initialize()" );
			
			return webAnalysis;
		}
		catch ( NoClassDefFoundError e ) {
			System.out.println( now() + " NoClassDefFoundError - Could not load constructor or initialize() function from " + fileName + "\n" );  
			throw ( e );
		}
		catch ( Exception e ) {
			System.out.println( now() + " Could not load constructor or initialize() function from " + fileName + "\n" );  
			throw ( e );
		}
	}
	
	/**
	 * read's the extra page information that will be passed into the processDocument() function  
	 *    
	 * @param fileName - this file contains the information about the page to be tested (url, headers, and statusCode)
	 * @return a SimulatedPageInfo object containing the extra information that will be passed into processDocument()
	 */
	private SimulatedPageInfo readPageInfo ( String fileName ) throws Exception {
	    SimulatedPageInfo info = new SimulatedPageInfo();
	    
	    if ( fileName == null ) {
	    	info.setUrl("http://www.80legs.com");
    		info.setStatusCodeLine ( "200" );
    		info.addHeader ( "DUMMY", "VALUE" );
	    }
	    else {
			BufferedReader input =  new BufferedReader(new FileReader(fileName));
			String line = null;
		    while (( line = input.readLine()) != null) {
		    	if ( line.startsWith("URL=") == true ) {
		    		info.setUrl( line.substring(line.indexOf("=")+1) );
		    	}
		    	if ( line.startsWith("STATUS=") == true ) {
		    		info.setStatusCodeLine ( line.substring(line.indexOf("=")+1) );
		    	}
		    	if ( line.startsWith("HEADER=") == true ) {
		    		String fullHeaderString = line.substring(line.indexOf("=")+1);
		    		String[] headerSplits = fullHeaderString.split(",",2);
		    		info.addHeader ( headerSplits[0], headerSplits[1] );
		    	}
		    }
		    if ( info.getUrl() == null ) {
		    	throw ( new Exception ( "URL= does not exist in " + fileName ) );
		    }
		    if ( info.getHeaders().size() == 0 ) {
		    	throw ( new Exception ( "HEADER= does not exist in " + fileName ) );
		    }
		    if ( info.getStatusCodeLine() == null ) {
		    	throw ( new Exception ( "STATUS= does not exist in " + fileName ) );
		    }
	    }
	    
	    return info;
	}

	/**
	 * reads the contents of the input file and returns them as a byte[]  
	 *    
	 * @param fileName - this file contains the user's data to be passed into WebAnalysis.deserializeData()
	 * @return a byte[] containing the contents of the file
	 */
	private byte[] readPageContents ( String fileName ) throws IOException {
		byte[] docContents;
		
		if ( fileName == null ) {
			String docContentsString = "This is just the default file contents.  To get real contents, use the arguments to the jar.";
			docContents = docContentsString.getBytes("UTF-8");
		}
		else {
			System.out.println ( now() + " Reading contents: " + fileName );
			int docContentsSize = (int)new File(fileName).length();
			docContents = new byte[docContentsSize];
			BufferedInputStream r = new BufferedInputStream(new FileInputStream(fileName));
			r.read(docContents);
			r.close();
		}
		
		return docContents;
	}

	/**
	 * tests the user's processDocument() function using the pageInfo and docContents
	 * the results are printed to stdout
	 *    
	 * @param pageInfo - a SimulatedPageInfo containing the url, headers, and statusCode
	 * @param docContents - the contents of the document to test
	 */
	private void testUserFunction ( SimulatedPageInfo pageInfo, byte[] docContents ) {
		
		try {
			// run the user's code
			System.out.println ( now() + " Starting parseLinks()" );
			Collection<String> pagesToCrawl = customerAnalysis.parseLinks(docContents, pageInfo.getUrl(), pageInfo.getHeaders(), null, pageInfo.getStatusCodeLine());
			if ( pagesToCrawl == null ) {
				System.out.println ( now() + " parseLinks() returned null - default 80legs link parsing would be used for this page." );
			}
			else {
				System.out.println ( now() + " parseLinks() returned " + pagesToCrawl.size() + " pagesToCrawl:" );
				for ( String pageToCrawl : pagesToCrawl ) {
					System.out.println ( "     " + pageToCrawl );
				}
			}
			System.out.println ( now() + " Finished parseLinks()" );
			
			System.out.println ( now() + " Starting processDocument()" );
			byte[] results = customerAnalysis.processDocument(docContents, pageInfo.getUrl(), pageInfo.getHeaders(), null, pageInfo.getStatusCodeLine());
			System.out.println ( now() + " Finished processDocument()" );
	
			printResults ( results );
		}
		catch ( Exception e ) {
			System.out.println ( now() + " got exception=" + e.getMessage());
			e.printStackTrace( System.out );
		}
	}
	
	/**
	 * prints the results as both a UTF-8 string (common case) and the individual bytes to stdout
	 *    
	 * @param results - the byte[] to be printed
	 */
	private void printResults ( byte[] results ) {
		if ( results == null ) {
			System.out.println ( now() + " Results from processDocument() are null.  This page will not be returned in the analyzed page results." );
			return;
		}
		
		try {
			System.out.println ( now() + " Results as a string " + new String ( results, "UTF-8" ) );
		}
		catch ( Exception e ){}
		
		System.out.println ( now() + " Result bytes:" );
		int numBytesPerLine = 16;
		for ( int ibyte = 0; ibyte < results.length; ibyte += numBytesPerLine ) {
			System.out.print("byte" + ibyte + "=");
			for ( int ibyteOnLine = 0; (ibyteOnLine < numBytesPerLine) && (ibyte+ibyteOnLine < results.length); ibyteOnLine++ ) {
				System.out.print((int)results[ibyte+ibyteOnLine] + ",");
			}
			System.out.print("\n");
		}
	}
	
	/**
	 * returns a String with the formatted current time
	 *    
	 * @return formatted current time
	 */
	private static String now() {
		final String DATE_FORMAT_NOW = "yyyy-MM-dd HH:mm:ss:SSS";
	    Calendar cal = Calendar.getInstance();
	    SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT_NOW);
	    return sdf.format(cal.getTime());
	}
}
